// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.linqingying.gitcode.util

import com.intellij.collaboration.api.ServerPath
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.project.Project
import com.intellij.util.io.URLUtil
import com.linqingying.gitcode.api.GitCodeServerPath
import com.linqingying.gitcode.authentication.accounts.GCAccountManager
import git4idea.remote.GitRemoteUrlCoordinates
import git4idea.remote.hosting.GitHostingUrlUtil.getUriFromRemoteUrl
import git4idea.remote.hosting.HostedGitRepositoriesManager
import git4idea.remote.hosting.discoverServers
import git4idea.remote.hosting.gitRemotesFlow
import git4idea.remote.hosting.mapToServers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.future.asDeferred
import org.jetbrains.annotations.VisibleForTesting
import java.net.URI

@Service(Service.Level.PROJECT)
class GCHostedRepositoriesManager(project: Project, cs: CoroutineScope) :
    HostedGitRepositoriesManager<GCGitRepositoryMapping> {

    @VisibleForTesting
    internal val knownRepositoriesFlow = run {
        val gitRemotesFlow = gitRemotesFlow(project).distinctUntilChanged()

        val accountsServersFlow = service<GCAccountManager>().accountsState.map { accounts ->
            mutableSetOf(GitCodeServerPath.DEFAULT_SERVER) + accounts.map { it.server }
        }.distinctUntilChanged()

        val discoveredServersFlow = gitRemotesFlow.discoverServers(accountsServersFlow) { remote ->
            findServerAt(LOG, remote) {
                val server = GitCodeServerPath.from(it.toString())
                val metadata = service<GCEnterpriseServerMetadataLoader>().loadMetadata(server).asDeferred().await()
                if (metadata != null) server else null
            }
        }.runningFold(emptySet<GitCodeServerPath>()) { accumulator, value ->
            accumulator + value
        }.distinctUntilChanged()

        val serversFlow = accountsServersFlow.combine(discoveredServersFlow) { servers1, servers2 ->
            servers1 + servers2
        }

        gitRemotesFlow.mapToServers(serversFlow) { server, remote ->
            GCGitRepositoryMapping.create(server, remote)
        }.onEach {
            LOG.debug("New list of known repos: $it")
        }
    }

    override val knownRepositoriesState: StateFlow<Set<GCGitRepositoryMapping>> =
        knownRepositoriesFlow.stateIn(cs, getStateSharingStartConfig(), emptySet())

    companion object {
        private val LOG = logger<GCHostedRepositoriesManager>()

        private fun getStateSharingStartConfig() =
            if (ApplicationManager.getApplication().isUnitTestMode) SharingStarted.Eagerly else SharingStarted.Lazily
    }
}

suspend fun <S : ServerPath> findServerAt(
    log: Logger,
    coordinates: GitRemoteUrlCoordinates,
    serverCheck: suspend (URI) -> S?
): S? {
    val uri = getUriFromRemoteUrl(coordinates.url)
    log.debug("Extracted URI $uri from remote ${coordinates.url}")
    if (uri == null) return null

    val host = uri.host ?: return null
    val path = uri.path ?: return null
    val pathParts = path.removePrefix("/").split('/').takeIf { it.size >= 2 } ?: return null
    val serverSuffix = if (pathParts.size == 2) null else pathParts.subList(0, pathParts.size - 2).joinToString("/")

    for (serverUri in listOf(
        URI(URLUtil.HTTPS_PROTOCOL, host, serverSuffix, null),
        URI(URLUtil.HTTP_PROTOCOL, host, serverSuffix, null),
        URI(URLUtil.HTTP_PROTOCOL, null, host, 8080, serverSuffix, null, null)
    )) {
        log.debug("Looking for server at $serverUri")
        try {
            val server = serverCheck(serverUri)
            if (server != null) {
                log.debug("Found server at $serverUri")
                return server
            }
        } catch (ignored: Throwable) {
        }
    }
    return null
}
